<?php
/**
 * 
 * A specialized version of Solar_Controller_Page for RESTful apps.
 * 
 * @category Lux
 * 
 * @package Lux_Controller
 * 
 * @author Rodrigo Moraes <rodrigo.moraes@gmail.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: Rest.php 140 2008-10-01 14:07:20Z rodrigo.moraes $
 * 
 */
class Lux_Controller_Rest extends Solar_Controller_Page
{
    /**
     * 
     * A list of supported HTTP request methods for each action, keyed by the
     * action name in "dashes" format. All public actions **must** be
     * explicitly listed here to be accessible. Example:
     * 
     * {{code: php
     * 
     *     protected $_action_http = array(
     *         // actionList() supports only GET
     *         'list' => 'GET',
     *         // actionItem() suppports all methods
     *         'item' => array('GET', 'POST', 'PUT', 'DELETE'),
     *     );
     * 
     * }}
     * 
     * @var array
     * 
     */
    protected $_action_http = array();
    
    /**
     * 
     * A list of actions to be mapped based on the HTTP request method,
     * keyed by the action name in "dashes" format. Example:
     * 
     * {{code: php
     * 
     *     // Normally 'actionItem()' is executed when '/my-controller/item'
     *     // is requested, but here we override this explicitly defining
     *     // actions for different request methods:
     *     protected $_action_map = array(
     *         'item' => array(
     *             // execute 'actionItemInsert()' if request method is POST
     *             'POST'   => 'item-insert',
     *             // execute 'actionItemUpdate()' if request method is PUT
     *             'PUT'    => 'item-update',
     *             // execute 'actionItemDelete()' if request method is DELETE
     *             'DELETE' => 'item-delete',
     *         ),
     *     );
     *
     * }}
     * 
     * @var array
     * 
     */
    protected $_action_map = array();
    
    /**
     * 
     * HTTP request method: 'GET', 'POST', 'PUT' or 'DELETE'.
     * 
     * @var string
     * 
     */
    protected $_request_method;
    
    /**
     * 
     * Content data to be converted based on the current format.
     * 
     * @var array
     * 
     */
    protected $_content;
    
    /**
     * 
     * Sets the response content based on the format.
     * 
     * @return void
     * 
     */
    protected function _render()
    {
        $this->_preRender();
        
        // make sure we have xml or json
        if ($this->_format != 'xml') {
            $this->_format = 'json';
        }
        
        if ($this->_format == 'xml') {
            $this->_renderXml();
        } else {
            $this->_renderJson();
        }
        
        $this->_postRender();
    }
    
    /**
     * 
     * Sets the response content for JSON.
     * 
     * @return void
     *
     * @todo make this method abstract?
     * 
     */
    protected function _renderJson()
    {
        // convert content to JSON
        $json = Solar::factory('Solar_Json');
        $this->_response->content = $json->encode($this->_content);
    }
    
    /**
     * 
     * Sets the response content for XML.
     * 
     * @return void
     *
     * @todo convert content to XML? make this method abstract?
     * 
     */
    protected function _renderXml()
    {
        $this->_response->content = $this->_content;
    }
    
    /**
     * 
     * Sets the value of $_content and defines as status code for the response.
     * 
     * @param array $content Content data to be converted to JSON or XML,
     * depending on the current format.
     * 
     * @param int $code Response status code.
     * 
     * @return void
     * 
     */    
    protected function _setContent($content, $code = 200)
    {
        $this->_content = (array) $content;
        $this->_response->setStatusCode($code);
    }
    
    // -----------------------------------------------------------------
    //
    // Validation and error handling.
    //
    // -----------------------------------------------------------------
    
    /**
     * 
     * Validates that the HTTP method is supported by the current action.
     * 
     * @return bool True if the current action supports the request method, 
     * false otherwise.
     * 
     */
    protected function _validateRequestMethod()
    {
        $action = $this->_action;
        $method = $this->_request_method;
        
        if (isset($this->_action_http[$action]) &&
            in_array($method, (array) $this->_action_http[$action])) {
            return true;
        }
        
        return false;
    }
    
    /**
     * 
     * Renders a error code and message and terminates the current script.
     * 
     * @param string Error message.
     * 
     * @param int $code Status code.
     * 
     */
    protected function _error($message, $code = 500)
    {
        $this->_setContent($message, $code);
        $this->_render();
        $this->_setContentType();
        
        echo $this->_response;
        exit((int) $code);
    }
    
    /**
     * 
     * Renders an exception message and terminates the current script.
     * 
     * @param Exception $e The exception encountered during fetch().
     *
     * @return void;
     * 
     */
    protected function _exceptionDuringFetch(Exception $e)
    {
        $this->_error($e->getMessage());
    }
    
    // -----------------------------------------------------------------
    //
    // Behavior hooks.
    //
    // -----------------------------------------------------------------
    
    /**
     * 
     * Executes after construction.
     * 
     * @return void
     * 
     */
    protected function _setup()
    {
        // set the request method
        switch (true) {
            case $this->_request->isGet():
                $this->_request_method = 'GET';
            break;
            case $this->_request->isPut():
                $this->_request_method = 'PUT';
            break;
            case $this->_request->isPost():
                $this->_request_method = 'POST';
            break;
            case $this->_request->isDelete():
                $this->_request_method = 'DELETE';
            break;
            default:
                $this->_error('ERR_INVALID_REQUEST_METHOD');
        }
    }
    
    /**
     * 
     * Executes before the first action.
     * 
     * @return void
     * 
     */
    protected function _preRun()
    {
        // convert first requested action name to dashes, to match keys in
        // $_action_map and $_action_http. the "dashes" format must be the
        // standard internally, e.g. for action names passed to _forward() etc
        // (code from Solar_Inflect::camelToDashes())
        $action = preg_replace('/([a-z])([A-Z])/', '$1 $2', $this->_action);
        $this->_action = str_replace(' ', '-', strtolower($action));
        
        // change action based on the request method, if defined
        $action = $this->_action;
        $method = $this->_request_method;
        if (isset($this->_action_map[$action][$method])) {
            $this->_action = $this->_action_map[$action][$method];
        }
    }
    
    /**
     * 
     * Executes before each action.
     * 
     * @return void
     * 
     */
    protected function _preAction()
    {
        // checks if the current action supports the request method
        if (! $this->_validateRequestMethod()) {
            $info = array(
                'action' => $this->_action,
                'method' => $this->_request_method,
            );
            
            if (isset($this->_action_http[$this->_action])) {
                $info['valid_methods'] = $this->_action_http[$this->_action];
            }
            
            throw $this->_exception('ERR_INVALID_REQUEST_METHOD', $info);
        }
        
        // extend this to add auth/access checkings
    }
    
    /**
     * 
     * Executes after each action.
     * 
     * @return void
     * 
     */
    protected function _postAction()
    {
    }
    
    /**
     * 
     * Executes after the last action.
     * 
     * @return void
     * 
     */
    protected function _postRun()
    {
    }
    
    /**
     * 
     * Executes before formatting the response content.
     * 
     * @return void
     * 
     */
    protected function _preRender()
    {
    }
    
    /**
     * 
     * Executes after formatting the response content.
     * 
     * @return void
     * 
     */
    protected function _postRender()
    {
    }
}
